/**
 * align dom node flexibly
 * @author yiminghe@gmail.com
 */

import utils from '../utils';
import getVisibleRectForElement from '../getVisibleRectForElement';
import adjustForViewport from '../adjustForViewport';
import getRegion from '../getRegion';
import getElFuturePos from '../getElFuturePos';

// http://yiminghe.iteye.com/blog/1124720

function isFailX(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.left < visibleRect.left ||
    elFuturePos.left + elRegion.width > visibleRect.right
  );
}

function isFailY(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.top < visibleRect.top ||
    elFuturePos.top + elRegion.height > visibleRect.bottom
  );
}

function isCompleteFailX(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.left > visibleRect.right ||
    elFuturePos.left + elRegion.width < visibleRect.left
  );
}

function isCompleteFailY(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.top > visibleRect.bottom ||
    elFuturePos.top + elRegion.height < visibleRect.top
  );
}

/**
 * flip的作用是对points数组中的每个元素进行处理，根据正则表达式reg进行匹配，并将匹配到的部分根据map映射进行替换，最后返回处理后的新数组。
 * 1. 在实现一个弹出框（tooltip）的位置调整时，可能需要根据可用空间翻转位置（如从top翻转到bottom，从left翻转到right）。
 * 2. 在CSS属性生成中，将逻辑方向（如起点、终点）转换为具体的物理方向（左、右）。
 * @param  points：一个字符串数组，每个元素是需要被处理的字符串。
 * @param  reg：一个正则表达式，用于匹配需要替换的部分。
 * @param  map：一个对象（或Map），其键为匹配到的字符串，值为替换后的字符串。
 * @return {[type]}
 */
function flip(points, reg, map) {
  const ret = [];
  utils.each(points, (p) => {
    ret.push(
      p.replace(reg, (m) => {
        return map[m];
      }),
    );
  });
  return ret;
}
/**
function flip(points, reg, map) {
  return points.map(p => {
    p.replace(reg, m => map[m] || m) // 如果`map[m]`不存在，则保留原字符串
  })
}
**/

// 翻转偏移量数组中指定索引处的值（取负）参数
function flipOffset(offset, index) {
  offset[index] = -offset[index];
  return offset;
}

// 将字符串形式的偏移量转换为数值。支持百分比和像素值。
function convertOffset(str, offsetLen) {
  let n;
  if (/%$/.test(str)) {
    n = (parseInt(str.substring(0, str.length - 1), 10) / 100) * offsetLen;
  } else {
    n = parseInt(str, 10);
  }
  return n || 0;
}

// 标准化偏移量数组（将数组中的两个元素从字符串转换为数值，转换规则使用convertOffset）
function normalizeOffset(offset, el) {
  offset[0] = convertOffset(offset[0], el.width);
  offset[1] = convertOffset(offset[1], el.height);
}

/**
 * doAlign是一个智能定位引擎，用于将源元素（el）精确对齐到目标区域（tgtRegion），同时确保元素在可视区域内可见。
 * 它会根据对齐配置自动处理边界溢出问题，并在必要时调整元素位置甚至尺寸。
 * 
 * @param el 需要定位的源元素
 * @param tgtRegion 参照节点所占的区域: { left, top, width, height }
 * @param align 对齐配置对象
 * @param isTgtRegionVisible 目标区域是否可见
 */
function doAlign(el, tgtRegion, align, isTgtRegionVisible) {
  // 1. 初始化变量： points（对齐点）、offset（源元素偏移）、targetOffset（目标偏移）、overflow（溢出配置）、source（源对象）
  let points = align.points;
  let offset = align.offset || [0, 0];
  let targetOffset = align.targetOffset || [0, 0];
  let overflow = align.overflow;
  const source = align.source || el;

  offset = [].concat(offset);
  targetOffset = [].concat(targetOffset);
  overflow = overflow || {};
  const newOverflowCfg = {};
  let fail = 0;

  // 2. 获取可见区域和元素区域
  const alwaysByViewport = !!(overflow && overflow.alwaysByViewport);
  // 当前节点可以被放置的显示区域
  const visibleRect = getVisibleRectForElement(source, alwaysByViewport);
  // 当前节点所占的区域, left/top/width/height
  const elRegion = getRegion(source);
  // 将 offset 转换成数值，支持百分比
  normalizeOffset(offset, elRegion);
  normalizeOffset(targetOffset, tgtRegion);

  // 3. 计算元素未来位置
  // 当前节点将要被放置的位置
  let elFuturePos = getElFuturePos(
    elRegion,
    tgtRegion,
    points,
    offset,
    targetOffset,
  );
  // 当前节点将要所处的区域
  let newElRegion = utils.merge(elRegion, elFuturePos);

  // 4. 处理溢出
  if (
    visibleRect &&
    (overflow.adjustX || overflow.adjustY) &&
    isTgtRegionVisible
  ) { // 如果设置了溢出调整（adjustX 或 adjustY）并且目标区域可见（isTgtRegionVisible），则进行调整
    // a. 水平方向调整（adjustX）
    if (overflow.adjustX) {
      // 如果横向不能放下
      if (isFailX(elFuturePos, elRegion, visibleRect)) {
        // 横向放不下： 尝试翻转水平方向（左右互换）和偏移量（offset和targetOffset的水平分量取负）
        // 对齐位置反下
        const newPoints = flip(points, /[lr]/gi, {
          l: 'r',
          r: 'l',
        });
        // 偏移量也反下
        const newOffset = flipOffset(offset, 0);
        const newTargetOffset = flipOffset(targetOffset, 0);
        // 重新计算新位置（newElFuturePos），如果新位置可以放下（不是完全失败），则采用新的对齐点、偏移量和目标偏移量。
        const newElFuturePos = getElFuturePos(
          elRegion,
          tgtRegion,
          newPoints,
          newOffset,
          newTargetOffset,
        );

        if (!isCompleteFailX(newElFuturePos, elRegion, visibleRect)) {
          fail = 1;
          points = newPoints;
          offset = newOffset;
          targetOffset = newTargetOffset;
        }
      }
    }

    // b. 垂直方向调整（adjustY）
    if (overflow.adjustY) {
      // 如果纵向不能放下
      if (isFailY(elFuturePos, elRegion, visibleRect)) {
        // 对齐位置反下: 尝试翻转垂直方向（上下互换）和偏移量的垂直分量
        const newPoints = flip(points, /[tb]/gi, {
          t: 'b',
          b: 't',
        });
        // 偏移量也反下
        const newOffset = flipOffset(offset, 1);
        const newTargetOffset = flipOffset(targetOffset, 1);
        // 重新计算新位置（newElFuturePos），如果新位置可以放下（不是完全失败），则采用新的对齐点、偏移量和目标偏移量。
        const newElFuturePos = getElFuturePos(
          elRegion,
          tgtRegion,
          newPoints,
          newOffset,
          newTargetOffset,
        );

        if (!isCompleteFailY(newElFuturePos, elRegion, visibleRect)) {
          fail = 1;
          points = newPoints;
          offset = newOffset;
          targetOffset = newTargetOffset;
        }
      }
    }

    // 如果失败，重新计算当前节点将要被放置的位置
    // c. 如果进行了调整（fail=1），则重新计算元素未来位置。
    if (fail) {
      elFuturePos = getElFuturePos(
        elRegion,
        tgtRegion,
        points,
        offset,
        targetOffset,
      );
      utils.mix(newElRegion, elFuturePos);
    }
    // d. 检查调整后是否仍然放不下（isStillFailX 或 isStillFailY）
    const isStillFailX = isFailX(elFuturePos, elRegion, visibleRect);
    const isStillFailY = isFailY(elFuturePos, elRegion, visibleRect);
    // 检查反下后的位置是否可以放下了，如果仍然放不下：
    // 1. 复原修改过的定位参数
    if (isStillFailX || isStillFailY) {
      let newPoints = points;

      // 重置对应部分的翻转逻辑
      if (isStillFailX) {
        newPoints = flip(points, /[lr]/gi, {
          l: 'r',
          r: 'l',
        });
      }
      if (isStillFailY) {
        newPoints = flip(points, /[tb]/gi, {
          t: 'b',
          b: 't',
        });
      }

      points = newPoints;

      offset = align.offset || [0, 0];
      targetOffset = align.targetOffset || [0, 0];
    }
    // 2. 只有指定了可以调整当前方向才调整
    newOverflowCfg.adjustX = overflow.adjustX && isStillFailX;
    newOverflowCfg.adjustY = overflow.adjustY && isStillFailY;

    // 确实要调整，甚至可能会调整高度宽度
    // e. 如果 newOverflowCfg 中有需要调整的方向，则调用 adjustForViewport调整元素的位置和大小（可能改变元素宽高）。
    if (newOverflowCfg.adjustX || newOverflowCfg.adjustY) {
      newElRegion = adjustForViewport(
        elFuturePos,
        elRegion,
        visibleRect,
        newOverflowCfg,
      );
    }
  }

  // 5. 调整元素尺寸：如果新的区域宽度或高度与原始区域不同，则通过CSS调整源元素的宽高。
  // need judge to in case set fixed with in css on height auto element
  if (newElRegion.width !== elRegion.width) {
    utils.css(
      source,
      'width',
      utils.width(source) + newElRegion.width - elRegion.width,
    );
  }

  if (newElRegion.height !== elRegion.height) {
    utils.css(
      source,
      'height',
      utils.height(source) + newElRegion.height - elRegion.height,
    );
  }

  // 6. 定位元素： 使用 utils.offset方法将元素定位到新的位置（newElRegion.left, newElRegion.top），并传入一些配置（如使用css right/bottom，使用css transform，忽略抖动等）。
  // https://github.com/kissyteam/kissy/issues/190
  // 相对于屏幕位置没变，而 left/top 变了
  // 例如 <div 'relative'><el absolute></div>
  utils.offset(
    source,
    {
      left: newElRegion.left,
      top: newElRegion.top,
    },
    {
      useCssRight: align.useCssRight,
      useCssBottom: align.useCssBottom,
      useCssTransform: align.useCssTransform,
      ignoreShake: align.ignoreShake,
    },
  );

  // 7. 返回结果: 返回一个对象，包含最终使用的对齐点（points）、偏移量（offset）、目标偏移量（targetOffset）和溢出配置（newOverflowCfg）。
  return {
    points,
    offset,
    targetOffset,
    overflow: newOverflowCfg,
  };
}

export default doAlign;
/**
 *  2012-04-26 yiminghe@gmail.com
 *   - 优化智能对齐算法
 *   - 慎用 resizeXX
 *
 *  2011-07-13 yiminghe@gmail.com note:
 *   - 增加智能对齐，以及大小调整选项
 **/
